﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AIOptimization.Algorithms.Repositories;

namespace AIOptimization.Algorithms
{
    public class ParticleSwarmOptimization
    {
        #region Properties
        bool parameterSeted;

        Random rd = null;
        List<Particle> swarm;
        short dimensions;
        int numberParticles;
        int iterationCount;
        double Xmin;
        double Xmax;
        double bestGlobalFitness;
        double[] bestGlobalPosition;
        double weight;
        double c1;
        double c2;
        double minV;
        double maxV;
        #endregion

        #region ctor
        public ParticleSwarmOptimization()
        {
            rd = new Random(0);
        }
        #endregion

        #region Must be used and override function
        public void SetParameter(int numberParticles, short dimensions, double Xmin, double Xmax, int iterationCount, double weight, double c1, double c2)
        {
            parameterSeted = true;
            swarm = new List<Particle>();
            this.numberParticles = numberParticles;
            this.dimensions = dimensions;
            this.Xmin = Xmin;
            this.Xmax = Xmax;
            this.iterationCount = iterationCount;
            this.c1 = c1;
            this.c2 = c2;
            this.weight = weight;

            minV = -1.0 * Xmax;
            maxV = Xmax;
            bestGlobalFitness = double.MaxValue;
            bestGlobalPosition = new double[dimensions];
        }

        public virtual double ObjectiveFunction(double[] X)
        {
            throw new Exception("You must be override this method. This method's your objective function. /r/n For example:f(x) = 3 + (x0^2 + x1^2) /r/n 3.0 + (x[0] * x[0]) + (x[1] * x[1]);");
        }
        #endregion

        #region Event Handler

        public delegate void NewIterationResultEvent(object o, PSOIterationEventArgs e);

        public class PSOIterationEventArgs : EventArgs
        {
            public List<Particle> Swarm { get; set; }
            public int IterationNumber { get; set; }

            public PSOIterationEventArgs(List<Particle> swarm, int iterationNumber)
            {
                Swarm = swarm;
                IterationNumber = iterationNumber;
            }
        }

        public event NewIterationResultEvent NewIterationResult;

        private void OnNewItertationResult(PSOIterationEventArgs e)
        {
            if (NewIterationResult != null)
                NewIterationResult(this, e);
        }
        #endregion

        public List<Particle> Execute()
        {
            if (!parameterSeted) throw new Exception("You must be set parameter. Using SetParameter Method");
            InitializeSwarm();
            FindBestGlobalPosition();
            IterationProcess();
            return swarm;
        }

        private void InitializeSwarm()
        {
            for (int i = 0; i < numberParticles; i++)
            {
                double fitness;
                double[] randomPosition = new double[dimensions];
                for (int j = 0; j < dimensions; j++)
                {
                    double low = Xmin;
                    double high = Xmax;
                    randomPosition[j] = (low * high) * rd.NextDouble() + low;
                }
                try
                {
                    fitness = ObjectiveFunction(randomPosition);
                }
                catch
                {
                    throw new Exception("Incorrect objective function. Function must be return double and have been argumen(double[])");
                }

                double[] randomVelocity = new double[dimensions];
                for (int j = 0; j < dimensions; j++)
                {
                    double low = -1.0 * Math.Abs(Xmax - Xmin);
                    double high = Math.Abs(Xmax - Xmin);
                    randomVelocity[j] = (high - low) * rd.NextDouble() + low;
                }
                swarm.Add(new Particle(randomPosition, fitness, randomVelocity, randomPosition, fitness));
            }
        }

        private double FindGlobalFitness()
        {
            var _bestfitness = swarm.OrderBy(s => s.BestFitness).FirstOrDefault();
            if (bestGlobalFitness > _bestfitness.BestFitness)
            {
                bestGlobalFitness = _bestfitness.BestFitness;
                bestGlobalPosition = _bestfitness.BestPosition;
            }
            return bestGlobalFitness;
        }

        private double[] FindBestGlobalPosition()
        {
            FindGlobalFitness();
            return bestGlobalPosition;
        }

        private void IterationProcess()
        {
            int iterationNum = 0;
            double[] tmpVelocity = new double[dimensions];
            double[] tmpPosition = new double[dimensions];
            double tmpFitness;
            double r1, r2;
            while (iterationNum < iterationCount)
            {
                ++iterationNum;
                foreach (Particle currentParticle in swarm)
                {
                    for (int j = 0; j < currentParticle.Velocity.Length; ++j)
                    {
                        r1 = rd.NextDouble();
                        r2 = rd.NextDouble();

                        tmpVelocity[j] = (weight * currentParticle.Velocity[j]) + (c1 * r1 * (currentParticle.BestPosition[j] - currentParticle.Position[j])) + (c2 * r2 * (bestGlobalPosition[j] - currentParticle.Position[j]));

                        if (tmpVelocity[j] < minV)
                            tmpVelocity[j] = minV;
                        else if (tmpVelocity[j] > maxV)
                            tmpVelocity[j] = maxV;
                    }

                    tmpVelocity.CopyTo(currentParticle.Velocity, 0);

                    for (int j = 0; j < currentParticle.Position.Length; ++j)
                    {
                        tmpPosition[j] = currentParticle.Position[j] + tmpVelocity[j];
                        if (tmpPosition[j] < Xmin)
                            tmpPosition[j] = Xmin;
                        else if (tmpPosition[j] > Xmax)
                            tmpPosition[j] = Xmax;
                    }
                    tmpPosition.CopyTo(currentParticle.Position, 0);

                    tmpFitness = ObjectiveFunction(tmpPosition);
                    currentParticle.Fitness = tmpFitness;

                    if (tmpFitness < currentParticle.BestFitness)
                    {
                        tmpPosition.CopyTo(currentParticle.BestPosition, 0);
                        currentParticle.BestFitness = tmpFitness;
                    }

                    if (tmpFitness < bestGlobalFitness)
                    {
                        tmpPosition.CopyTo(bestGlobalPosition, 0);
                        bestGlobalFitness = tmpFitness;
                    }
                }
                OnNewItertationResult(new PSOIterationEventArgs(swarm,iterationNum));
            }
        }

    }
}
